fix: marketplace install auth host on *.ghe.com (closes #1285)#1292
fix: marketplace install auth host on *.ghe.com (closes #1285)#1292edenfunf wants to merge 3 commits into
Conversation
Marketplaces registered on `*.ghe.com` (GHE Cloud) resolved plugin install auth against `github.com` instead of the registered enterprise host. The marketplace resolver emitted a canonical without the host prefix (`owner/repo/path`); `DependencyReference.parse` defaults missing hosts to `github.com`, mis-routing every downstream auth lookup -- and poisoning the resulting `apm.yml` entry with the wrong `git:` URL. `_marketplace_host_needs_explicit_git_path` conflated two orthogonal concerns. GitHub-family hosts (github.com + *.ghe.com) correctly skip the GitLab-style structured-ref path because they have no nested-group ambiguity -- but the same gate also dropped the host hint needed by `DependencyReference.parse` to recover the correct enterprise host. `github.com` survived because parse defaults to it; `*.ghe.com` broke. Backfill the host onto the canonical for in-marketplace sources only, scoped to GitHub-family enterprise hosts. Idempotent against dict sources that already carry a host-qualified `repo`. Cross-repo sources without explicit host qualification stay out of scope -- they are a separate manifest-author concern and inferring host there would change existing semantics. Round-trip stable through `DependencyReference.parse` / `to_canonical()`, so `apm.yml` lockfile entries now record the correct enterprise `git:` URL instead of the (silently wrong) `https://github.com/...` URL produced before the fix.
There was a problem hiding this comment.
Pull request overview
Fixes marketplace plugin installs for GHE Cloud (*.ghe.com) hosts by ensuring the resolved canonical reference carries the enterprise host prefix, so downstream DependencyReference.parse() (and auth routing) does not default to github.com (closes #1285).
Changes:
- Add
_needs_canonical_host_prefix()and a scoped host-prefix backfill inresolve_marketplace_plugin()for GitHub-family enterprise hosts. - Add a focused unit test suite covering GHE Cloud canonical host prefixing, idempotency, cross-repo non-prefixing, and version ref override behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/apm_cli/marketplace/resolver.py |
Adds helper + conditional canonical host-prefix backfill for *.ghe.com marketplace resolutions. |
tests/unit/marketplace/test_marketplace_resolver.py |
Adds unit tests validating correct canonical host behavior for GHE Cloud marketplaces and key regressions. |
Manifests that put a full URL or SSH SCP shorthand in a dict source's `repo` field reach `_needs_canonical_host_prefix` via `_resolve_github_source` (which validates only that `/` is present) and `_is_in_marketplace_source` (whose normalizer accepts URL/SSH paths). Before this guard the prefix step produced malformed canonicals like `corp.ghe.com/https://corp.ghe.com/...` that `DependencyReference.parse` rejects with `ValueError` -- a regression versus the pre-fix tolerance where parse recovered host from the URL form natively. Detect URL/SSH form by `:` in the first slash-split segment of the canonical (both `https:` and `git@host:owner` carry a colon; bare owner names and bare host shorthand do not) and return False, leaving the canonical untouched. Downstream parse already handles both URL and SSH forms natively for routing, so no host backfill is needed.
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 1 | Minimal, well-scoped fix; guard logic is correct; one unaddressed cross-repo GHE edge case worth noting; test suite is thorough. |
| CLI Logging Expert | 0 | 1 | 1 | Backfill path is silent while every comparable resolver branch emits logger.debug; add one debug line so --verbose traces auth routing decisions for *.ghe.com. |
| DevX UX Expert | 0 | 2 | 1 | Happy-path GHE fix is zero-config and transparent; cross-repo GHE plugins still silently mis-route auth with no actionable error surface. |
| Supply Chain Security Expert | 0 | 1 | 1 | Fix correctly routes enterprise host auth; host sourced from trusted MarketplaceSource, not manifest content; one residual auth-routing gap for cross-repo plugins. |
| OSS Growth Hacker | 0 | 1 | 2 | GHE marketplace auth fix is a high-value enterprise unlock -- but missing CHANGELOG entry means enterprise teams won't discover it on upgrade. |
| Auth Expert | 0 | 1 | 1 | Fix is auth-correct: *.ghe.com canonical host backfill routes enterprise token; GHES/cross-host guards are sound; no token cross-contamination risk. |
| Doc Writer | 0 | 2 | 1 | CHANGELOG missing #1285 entry; consumer auth doc has no mention of *.ghe.com marketplace auth fallback bug; module docstring is accurate. |
| Test Coverage Expert | 0 | 1 | 0 | 9 unit tests cover helper + backfill thoroughly; integration-tier regression trap for GHE auth routing is missing (floor: integration-with-fixtures for auth resolution). |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [OSS Growth Hacker + Doc Writer] Add CHANGELOG
[Unreleased]Fixed entry for [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285 -- Convergent signal from two personas. Enterprise teams who pinned workarounds for [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285 will never discover this fix without a changelog entry. Suggested copy: "apm marketplace installon*.ghe.comhosts now uses enterprise credentials instead of silently falling back togithub.comauth (closes [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285)". Zero-cost, high-enterprise-trust ROI. - [Test Coverage Expert] Add integration-with-fixtures regression trap for GHE marketplace auth routing -- Evidence-backed missing test (
outcome: missing) on a secure-by-default + governed-by-policy surface. The full flowapm install->resolve_marketplace_plugin-> canonical ->DependencyReference.parse-> auth credential lookup is not exercised by any test intests/integration/. Suggested file:tests/integration/marketplace/test_ghe_marketplace_install_e2e.py. - [Python Architect + DevX UX Expert + Supply Chain Security Expert] Emit
logger.warningfor cross-repo GHE plugins that will silently mis-route auth -- Three independent panelists flag this pre-existing gap. When_is_in_marketplace_sourcereturns False for a*.ghe.commarketplace and the canonical is bareowner/repo, the downstream 401 is invisible to the user. A single warning with a host-qualify hint converts a silent failure into an actionable error and prevents repeat [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285-class support tickets. - [CLI Logging Expert] Add
logger.debugon the host-prefix backfill path -- The backfill block is the exact scenario operators will trace with--verbosewhen debugging*.ghe.comauth failures. Every comparable resolver branch emits a debug line. Suggested:logger.debug("Backfilled enterprise host '%s' onto canonical for %s@%s (auth routing #1285)", source.host, plugin_name, marketplace_name). - [Auth Expert] Add docstring note explaining why GHES (
GITHUB_HOST) hosts are intentionally excluded from_needs_canonical_host_prefix-- GHES hosts correctly take thedep_refpath, but a developer reading the function will not know this without an explicit note. Prevents future developers from re-introducing the same class of auth-routing bug.
Architecture
classDiagram
direction LR
class resolve_marketplace_plugin {
<<EntryPoint>>
+plugin_name str
+marketplace_name str
+version_spec str
+auth_resolver AuthResolver
+returns MarketplacePluginResolution
}
class MarketplacePluginResolution {
<<ValueObject>>
+canonical str
+plugin MarketplacePlugin
+dependency_reference DependencyReference
}
class MarketplaceSource {
<<ValueObject>>
+host str
+owner str
+repo str
+branch str
}
class MarketplacePlugin {
<<ValueObject>>
+name str
+source str
+version str
}
class DependencyReference {
<<ValueObject>>
+host str
+repo_url str
+virtual_path str
+reference str
+parse(canonical) DependencyReference
+to_canonical() str
}
class _needs_canonical_host_prefix {
<<Pure>>
+canonical str
+host str
+returns bool
}
class _is_in_marketplace_source {
<<Pure>>
+plugin MarketplacePlugin
+source MarketplaceSource
+returns bool
}
class is_github_hostname {
<<Pure>>
+hostname str
+returns bool
}
resolve_marketplace_plugin *-- MarketplacePluginResolution : produces
resolve_marketplace_plugin ..> MarketplaceSource : reads
resolve_marketplace_plugin ..> MarketplacePlugin : reads
resolve_marketplace_plugin ..> _is_in_marketplace_source : calls
resolve_marketplace_plugin ..> _needs_canonical_host_prefix : calls
resolve_marketplace_plugin ..> DependencyReference : delegates parse
_needs_canonical_host_prefix ..> is_github_hostname : delegates
MarketplacePluginResolution o-- DependencyReference : optional
class _needs_canonical_host_prefix:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A(["apm install plugin@marketplace"]) --> B["resolve_marketplace_plugin()"]
B --> C["fetch_or_cache() -- fetch marketplace manifest"]
C --> D["resolve_plugin_source() -- build bare canonical: owner/repo/path"]
D --> E{"_marketplace_host_needs_explicit_git_path(source.host)?"}
E -->|"Yes - GitLab/generic"| F["dep_ref set via _gitlab_in_marketplace_dependency_reference()"]
E -->|"No - github.com or *.ghe.com"| G["dep_ref = None, canonical stays bare"]
F --> H{"dep_ref is None?"}
G --> H
H -->|"No - GitLab path"| I["skip host backfill"]
H -->|"Yes"| J{"_is_in_marketplace_source() AND _needs_canonical_host_prefix()?"}
J -->|"False: github.com, cross-repo, or already host-prefixed"| K["canonical unchanged"]
J -->|"True: *.ghe.com in-marketplace -- FIX #1285"| L["canonical = source.host/canonical"]
I --> M{"version_spec override?"}
K --> M
L --> M
M -->|"Yes"| N["canonical = base#version_spec"]
M -->|"No"| O["MarketplacePluginResolution(canonical, plugin, dep_ref)"]
N --> O
O --> P["DependencyReference.parse(canonical) -- routes auth to host in canonical"]
P --> Q(["Auth at correct GHE host"])
Recommendation
Ship this PR. The fix is auth-correct, security-positive, and transparent to users -- auth-expert and supply-chain-security both confirm no token cross-contamination risk and a correctly scoped trust boundary. Unit test quality is HIGH with real DependencyReference.parse boundary assertions. The five follow-ups are all non-blocking improvements: the CHANGELOG entry is the highest-urgency item (open a follow-up commit or PR in the same release window); the integration-with-fixtures test should be tracked as a follow-up issue with the secure-by-default label; the cross-repo warning, debug log, and docstring clarification can land in the same minor release. None of these gaps were introduced by this PR, and none represent regressions from the pre-PR state.
Full per-persona findings
Python Architect
-
[recommended] Cross-repo GHE plugins silently misroute auth and the limitation is only documented in a test comment at
src/apm_cli/marketplace/resolver.py
When a*.ghe.commarketplace lists a cross-repo plugin,_is_in_marketplace_sourcereturns False and_needs_canonical_host_prefixis never reached. The bare canonical propagates andDependencyReference.parsedefaults togithub.com, causing the same auth-routing failure this PR fixes for in-marketplace sources. The testtest_cross_repo_source_not_prefixeddocuments this as intentional but the limitation is invisible to users.
Suggested: Add alogger.debugorlogger.warninginresolve_marketplace_pluginwhensource.hostis a*.ghe.comhost and a cross-repo dict source is detected without a host-qualified repo field. -
[nit] Guard order in
_needs_canonical_host_prefixcould surface the idempotency check before the colon check atsrc/apm_cli/marketplace/resolver.py
Thefirst_segment.lower() != h.lower()idempotency check is the most semantically important guard; currently it is last, after the:check. Reordering is a no-op at runtime but reads top-to-bottom more clearly.
CLI Logging Expert
-
[recommended] Missing
logger.debugfor host-prefix backfill path atsrc/apm_cli/marketplace/resolver.py
The immediately adjacent raw-ref override block emitslogger.debugwith plugin/marketplace/ref context. The backfill block is the exact scenario operators will need to trace when debugging*.ghe.comauth failures -- yet it is completely silent.
Suggested: Aftercanonical = f"{source.host}/{canonical}"add:logger.debug("Backfilled enterprise host '%s' onto canonical for %s@%s (auth routing #1285)", source.host, plugin_name, marketplace_name) -
[nit]
warning_handleris not threaded to_needs_canonical_host_prefixcallers -- purely cosmetic, no action required.
DevX UX Expert
-
[recommended] Cross-repo GHE plugin: silent auth mis-route with no recovery hint at
src/apm_cli/marketplace/resolver.py
When a GHE marketplace user lists a cross-repo plugin without host qualification,DependencyReference.parsedefaults togithub.com, auth fails, and the user receives no signal. A one-line warning when_is_in_marketplace_sourcereturns False butsource.hostis a*.ghe.comhost would surface a concrete next action.
Suggested: Text: "Plugin X source Y is outside the marketplace repository; if it lives on {host}, qualify the repo field as {host}/{repo} for correct authentication." -
[recommended] No validation or error annotation when canonical is still auth-unresolvable at install time at
src/apm_cli/marketplace/resolver.py
If for any reason the backfill is skipped, the downstream failure is a generic clone/auth error. A structured install-time check comparingdep.hostvssource.hostwould give users one copy-pasteable correction instead of a raw git auth failure. -
[nit]
_needs_canonical_host_prefixdocstring is longer than the function body -- accurate but slow to scan; consider a one-line summary + inline comments at key guards.
Supply Chain Security Expert
-
[recommended] Cross-repo plugins on
*.ghe.commarketplaces still mis-route auth atsrc/apm_cli/marketplace/resolver.py
A cross-repo plugin hosted on the same*.ghe.cominstance will still emit a bare canonical, soDependencyReference.parsedefaults togithub.comcredentials. Pre-existing behaviour, not introduced by this PR. -
[nit]
is_github_hostnamenote about "any FQDN" could mislead future readers -- a one-liner distinguishing*.ghe.com(GHE Cloud) from GHES (arbitrary FQDN) would close the reading gap atsrc/apm_cli/utils/github_host.py.
OSS Growth Hacker
-
[recommended] No CHANGELOG entry for [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285 fix at
CHANGELOG.md
Enterprise GHE teams who hit [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285 and pinned a workaround will never know this was fixed without a changelog line. -
[nit] Marketplace CLI reference docs have no sentence confirming
*.ghe.comis a supported marketplace host. -
[nit] Story angle: "
*.ghe.commarketplace installs now use your enterprise credentials" is a release-note one-liner worth amplifying in the next release post.
Auth Expert
-
[recommended] Docstring should clarify why GHES (
GITHUB_HOST) hosts are intentionally excluded from_needs_canonical_host_prefixatsrc/apm_cli/marketplace/resolver.py
GHES hosts return False fromis_github_hostname, which is correct because they already take thedep_refpath. A one-sentence note closes the reading gap and prevents future developers from treating this as a missing case. -
[nit] String-source shortcut in
_is_in_marketplace_sourcebypasses host validation --isinstance(s, str) -> return Trueunconditionally, so a mis-authored manifest entry could receive an unexpected enterprise host prefix. Pre-existing, outside this PR's scope.
Doc Writer
-
[recommended] CHANGELOG
[Unreleased]missing entry for [BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285 atCHANGELOG.md
The[Unreleased]Fixed section has entries but none for this user-visible bug fix. -
[recommended]
consumer/authentication.mdhas no mention of*.ghe.commarketplace-specific auth routing atdocs/src/content/docs/consumer/authentication.md
The doc covers*.ghe.comauth for direct installs but not marketplace-sourced installs. A user who hits this bug will consult this page and remain confused. -
[nit] Module docstring: issue reference
(#1285)in RST docstring is non-standard and won't render as a hyperlink -- consider dropping or using a.. versionchanged::directive.
Test Coverage Expert
- [recommended] No integration-with-fixtures regression trap for GHE marketplace auth routing at
tests/integration/marketplace/test_ghe_marketplace_install_e2e.py
The bug fixed here is user-reported ([BUG] Marketplace install fails for *.ghe.com hosts — auth resolves against github.com instead of registered host #1285). The full flowapm install->resolve_marketplace_plugin-> canonical ->DependencyReference.parse-> auth credential lookup is not exercised by any test intests/integration/. Grepped forresolve_marketplace_plugin,MarketplaceSource.*ghe,ghe.*install-- zero hits. The 9 unit tests are HIGH quality (realDependencyReference.parseboundary assertions) but do not certify the end-to-end auth routing promise.
Proof (missing at integration-with-fixtures):tests/integration/marketplace/test_ghe_marketplace_install_e2e.py-- proves:apm installwith a*.ghe.commarketplace source resolves credentials at the enterprise host, notgithub.com[secure-by-default, governed-by-policy]
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 2 items
The following items were blocked because they don't meet the GitHub integrity level.
- #1292
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - fix: marketplace install auth host on *.ghe.com (closes #1285) #1292
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #1292 · ● 2.9M · ◷
…low-up Three non-behavioral additions in response to PR review feedback: - CHANGELOG `[Unreleased] Fixed` entry so enterprise teams who pinned workarounds for the silent github.com auth fallback can discover the fix on upgrade (they will not, otherwise -- this is the highest-value follow-up surfaced by the review panel). - `logger.debug` on the host backfill path. The catch-all `Resolved %s@%s -> %s` already shows the prefixed canonical at the end of resolution, but does not explain WHY the prefix is there. Distinguishing "source emitted host-qualified" from "resolver backfilled host" matters for operators tracing future microsoft#1285-class auth-routing issues with `--verbose`. - Docstring paragraph on `_needs_canonical_host_prefix` explaining why GHES (`GITHUB_HOST` self-managed) is not handled here -- GHES takes the structured `dep_ref` path upstream and never reaches this helper. Prevents a future developer from extending the helper's condition set in a way that conflicts with the upstream structured-ref path. Cross-repo silent mis-route warning and the integration-test regression trap suggested by the review panel are tracked as separate follow-up issues, intentionally not bundled here: the cross-repo warning needs install-time error-handler placement (a resolver-time always-on warning would false-positive on intentional github.com cross-host references), and the integration test needs fixture/marker infrastructure that inflates this PR's scope without changing the fix's correctness.
f3bee6d to
9596af7
Compare
TL;DR
For marketplaces registered on
*.ghe.com(GHE Cloud) hosts, plugin install resolved auth againstgithub.cominstead of the registered enterprise host. This change backfills the marketplace host on the resolver's canonical string soDependencyReference.parserecovers the correct host downstream. Closes #1285.Problem
apm install <plugin>@<marketplace>against a marketplace registered with--host corp.ghe.comfailed with an authentication error. The verbose trace showed:DependencyReference.parsedefaults missing hosts togithub.com, so every*.ghe.commarketplace mis-routes auth. The same plugin installed via fully-qualified path worked correctly:i.e. the rest of the install pipeline already handles GHE Cloud correctly; only the marketplace → canonical step dropped the host.
A second, quieter symptom: the bare canonical also produced an
apm.ymlentry withgit: https://github.com/...even though the marketplace was oncorp.ghe.com, poisoning the lockfile with the wrong origin.Root cause
resolve_marketplace_pluginuses_marketplace_host_needs_explicit_git_path(source.host)to decide whether to build a structuredDependencyReference(explicitgit:URL +path:). That helper returnsFalsefor any GitHub-family host (github.com+*.ghe.com) because shorthandowner/repo[/path]is unambiguous on those hosts -- no GitLab-style nested-group ambiguity.The flaw: that single condition conflated two orthogonal concerns.
git:+path:to disambiguate the clone target? No for GitHub family. ✓DependencyReference.parseto recover the correct host? No for*.ghe.com; yes forgithub.combecause it is the documentedparsedefault.Both concerns were gated on the same return value, so
*.ghe.comcorrectly skipped the structured-ref path but incorrectly also dropped the host hint.Approach
Backfill the host onto the canonical post-hoc, scoped narrowly. The structured-ref path for GitLab-family hosts is left untouched -- this only fills the gap for the GitHub-family enterprise branch.
A new pure helper
_needs_canonical_host_prefix(canonical, host)answers a single question; the call site inresolve_marketplace_pluginadds five lines after the existing structured-ref block:Three guards justify the scope:
dep_ref is None-- the structured-ref path (GitLab family, self-managed FQDN) already carries host viato_canonical(). Don't double-handle._is_in_marketplace_source(plugin, source)-- only sources whose host is unambiguously the registered marketplace host get backfilled. Cross-repo dict sources without explicit host qualification are deliberately untouched; treating them as on-host would silently change routing for existing manifests._needs_canonical_host_prefix(canonical, host)-- narrows to GitHub-family enterprise hosts (is_github_hostname(host) and host != "github.com") and is idempotent (case-insensitive) against dict sources that already carry a host-qualifiedrepo.dependency_referenceremainsNonefor GitHub-family hosts -- the clone-path shorthand semantics are preserved, only the auth-routing string is corrected.Tests
New class
TestResolveMarketplacePluginGHECloudintests/unit/marketplace/test_marketplace_resolver.py(7 cases). Each test locks one branch of the helper or one invariant the fix must preserve:test_relative_source_carries_host_in_canonicalcorp.ghe.comtest_dict_github_source_bare_repo_carries_hosttest_dict_github_source_host_qualified_repo_not_double_prefixedtest_dict_github_source_mixed_case_host_qualified_not_double_prefixedtest_cross_repo_source_not_prefixedtest_version_spec_override_preserves_host_prefixtest_github_com_canonical_remains_bareValidation
End-to-end equivalence check against real
AuthResolver(mock marketplace fetch only):myorg/my-marketplace/plugins/my-plugincorp.ghe.com/myorg/my-marketplace/plugins/my-plugincorp.ghe.com/myorg/my-marketplace/plugins/my-plugingithub.comcorp.ghe.comcorp.ghe.comgithubghe_cloudghe_cloudhttps://github.com/myorg/my-marketplacehttps://corp.ghe.com/myorg/my-marketplacehttps://corp.ghe.com/myorg/my-marketplacehttps://github.com/...https://corp.ghe.com/...https://corp.ghe.com/...After this change the marketplace path produces byte-identical resolver / auth / lockfile output to the documented workaround (
apm install corp.ghe.com/owner/repo/path).How to test
For a manual reproduction:
*.ghe.comhost:apm marketplace add --host corp.ghe.com myorg/my-marketplaceapm install my-plugin@my-marketplace --verboseResolved to: myorg/my-marketplace/plugins/my-pluginandAuth resolved: host=github.com(auth fails).Resolved to: corp.ghe.com/myorg/my-marketplace/plugins/my-pluginandAuth resolved: host=corp.ghe.com(auth succeeds).